package main

import (
	"compress/gzip"
	"crypto/hmac"
	"crypto/sha256"
	"database/sql"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"net/url"
	"os"
	"regexp"
	"strconv"
	"strings"
	"time"
	_ "github.com/mattn/go-sqlite3"
)

var (
	urlHost       = "http://picaapi.picacomic.com/"
	urlHot24      = "comics/leaderboard?tt=H24&ct=VC"
	nonce         = "4e6453b519da4472ba05507633a4470c"
	appKey        = "C69BAF41DA5ABD1FFEDC6D2FEA56B"
	authorization string
	path          = "G:\\picaComic\\"
	//imgServer     = "https://wakamoment.ml/static/"
	//imgServer     = "https://media.wakamoment.tk/static/"
	imgServer     = "https://storage.wikawika.xyz/static/"
	comicList     = make(map[string]*comic)
	keyScan       string
	reg, _        = regexp.Compile(`[\\/:\*\?"< >|]`)
	downIndex     int
	downTot       int
	dbPath = "./picaDown.db"
	selectComicSql = "SELECT PAGES,IS_FINISHED FROM COMICS WHERE ID = ?"
	searchPage = 1
	searchKey string
)

func main() {
	http.DefaultClient.Timeout = 10 * time.Second
	for ; ; {
		menu()
		_, _ = fmt.Scanln(&keyScan)
		switch keyScan {
		case "init":
			fmt.Printf("===============================\r\n初始化数据库\r\n-------------------------------\r\n")
			initDb()
			println("===============================\r\n")
		case "u":
			fmt.Printf("===============================\r\n更新本子：%s\r\n-------------------------------\r\n","WEBTOON")
			updateComics("WEBTOON")
			println("===============================\r\n")
		case "1":
			fmt.Printf("===============================\r\n下载最新本子\r\n-------------------------------\r\n")
			newFinishedComics(nil,true,"WEBTOON")
			fail,_ := downFailComics("")
			for l:=range fail{
				downComic(l,nil)
			}
			println("===============================\r\n")
		case "2":
			fmt.Printf("===============================\r\n未下载的完结本子\r\n-------------------------------\r\n")
			newFinishedComics(nil,false,"")
			println("===============================\r\n")
		case "e":
			fmt.Printf("===============================\r\n下载不完整的本子\r\n-------------------------------\r\n")
			fail,_ := downFailComics("")
			for l:=range fail{
				println(l)
			}
			println("===============================\r\n")
		case "h":
			fmt.Printf("===============================\r\nhot24\r\n-------------------------------\r\n")
			hot24()
			println("===============================\r\n")
		case "s":
			fmt.Printf("===============================\r\nkey,page\r\n-------------------------------\r\n")
			_, _ = fmt.Scanln(&keyScan)
			keys := strings.Split(keyScan,",")
			page := 1
			if len(keys)>=2 {
				page,_ = strconv.Atoi(keys[1])
			}
			if keys[0]=="s"{
				searchPage = searchPage +1
			}else{
				searchKey = keys[0]
				searchPage = page
			}
			search(searchKey, searchPage)
			println("===============================\r\n")
		case "w":
			fmt.Printf("===============================\r\n未下载的完结本子WEBTOON\r\n-------------------------------\r\n")
			newFinishedComics(nil,false,"WEBTOON")
			println("===============================\r\n")
		case "0":
			os.Exit(0)
		case "t":
			//fail,_:=downFailComics("5c5ee2c738565d0d46ac0df0','5c5d40071b43e92b561cd1bb','5c5d3ff8ad486f7e5da2adc6','5c5d3c4096f1c60594bf67f7','5c5d3c1696f1c60594bf6708','5c5a48191ccbff6568fc93dd','5c5a480f310ff45364db86f9")
			//for l:=range fail{
			//	println(l)
			//}
			err := os.Remove("G:\\picaComic\\webtoon\\test.pdf")
			println(err)
		default:
			if len(keyScan) >= 24 {
				//fmt.Printf("===============================\r\n下载本子：%s\r\n-------------------------------\r\n",keyScan)
				fmt.Println("===============================\r\n-------------------------------")
				list := strings.Split(keyScan,",")
				for _,l:=range list{
					downComic(l,nil)
				}
				fail,_ := downFailComics(strings.Join(list,"','"))
				for l:=range fail{
					downComic(l,nil)
				}

				println("===============================\r\n")
			}
		}

	}
	//initDb()
	//updateComics("webtoon")
	//iteratorDb()
	fmt.Scanln()
}

func printErr(tar string,err error){
	println(tar + ": " + err.Error())
}

func menu() {
	println("===============================")
	println("哔咔漫画")
	println("-------------------------------")
	println("【init. 初始化】【1. 下载最新本子】【2. 未下载的完结本子】【0. 退出】")
	println("【u. 更新本子】【h. hot24】【w. 未下载完结WEBTOON】【e. 下载不完整的本子】【s. search key,page】")
	println("===============================\r\n")
}

func initDb() error{
	db,err := sql.Open("sqlite3", dbPath)
	defer db.Close()
	if err != nil {
		printErr("initDb", err)
		return err
	}else{
		db.Exec("CREATE TABLE IF NOT EXISTS COMICS(ID VARCHAR(64) PRIMARY KEY,TITLE VARCHAR(64),PAGES INTEGER,IS_FINISHED BIT,IS_NEW BIT,IS_DOWN BIT)")
		db.Exec("CREATE TABLE IF NOT EXISTS PICS(ID VARCHAR(64) PRIMARY KEY,PATH VARCHAR(64),COMIC_ID VARCHAR(64),IS_DOWN BIT )")
	}
	return nil
}

//搜索
func search(key string, page int)error {
	db,err := sql.Open("sqlite3", dbPath)
	defer db.Close()
	if err != nil{
		printErr("hot24", err)
		return err
	}

	keyUrlCode := url.QueryEscape(key)
	response,err := picaRequest("comics/search?page="+strconv.Itoa(page)+"&q="+keyUrlCode, "GET")
	if err != nil{
		printErr("search", err)
		return err
	}

	var jsonObj searchResponse
	err = json.Unmarshal([]byte(response), &jsonObj)
	if err != nil {
		fmt.Println("err", err)
	}

	for _, c := range jsonObj.Data.Comics.Docs {
		_, err := db.Exec("REPLACE INTO COMICS(ID,TITLE,PAGES,IS_FINISHED,IS_NEW,IS_DOWN,LABELS) VALUES(?,?,?,?,?,?,?)",c.Id,reg.ReplaceAllString(c.Title,"-"),c.PagesCount,c.Finished,1,0,strings.Join(c.Categories," "))
		if err != nil {
			printErr("hot24", err)
			return err
		}

		finish := ""
		if c.Finished {
			finish = "*完结*"
		}
		cc := c
		comicList[c.Id] = &cc
		println(c.Id, c.Title, "【"+strings.Join(c.Categories, ",")+"】", strconv.FormatInt(int64(c.EpsCount), 10)+"集", strconv.FormatInt(int64(c.PagesCount), 10)+"图", finish)
	}

	return nil
}

//24小时最热
func hot24()error {
	db,err := sql.Open("sqlite3", dbPath)
	defer db.Close()
	if err != nil{
		printErr("hot24", err)
		return err
	}

	response, err := picaRequest(urlHot24, "GET")
	if err != nil{
		printErr("hot24", err)
		return err
	}

	var jsonObj hot24Response
	err = json.Unmarshal([]byte(response), &jsonObj)
	if err != nil {
		printErr("hot24", err)
		return err
	}

	for _, c := range jsonObj.Data.Comics {
		_, err := db.Exec("REPLACE INTO COMICS(ID,TITLE,PAGES,IS_FINISHED,IS_NEW,IS_DOWN,LABELS) VALUES(?,?,?,?,?,?,?)",c.Id,reg.ReplaceAllString(c.Title,"-"),c.PagesCount,c.Finished,1,0,strings.Join(c.Categories," "))
		if err != nil {
			printErr("hot24", err)
			return err
		}

		finish := ""
		if c.Finished {
			finish = "*完结*"
		}
		cc := c
		comicList[c.Id] = &cc
		println(c.Id, c.Title, "【"+strings.Join(c.Categories, ",")+"】", strconv.FormatInt(int64(c.EpsCount), 10)+"集", strconv.FormatInt(int64(c.PagesCount), 10)+"图", finish)
	}
	return nil
}

//更新本子列表
func updateComics(search string) error{
	db,err := sql.Open("sqlite3", dbPath)
	defer db.Close()
	if err != nil {
		printErr("updateComics", err)
		return err
	}else{
		err = updateComicsPage(&search,1, db)
		if err != nil{
			printErr("updateComics", err)
			return err
		}
	}
	newFinishedComics(db,false,"WEBTOON")
	return nil
}

//更新本子列表每页
func updateComicsPage(search *string, page int,db *sql.DB)error{
	keyUrlCode := url.QueryEscape(*search)
	//response,err := picaRequest("comics/search?page="+strconv.Itoa(page)+"&q="+keyUrlCode, "GET")
	response,err := picaRequest("comics?page="+strconv.Itoa(page)+"&c="+keyUrlCode, "GET")
	if err != nil{
		printErr("updateComicsPage", err)
		return err
	}

	var jsonObj searchResponse
	err = json.Unmarshal([]byte(response), &jsonObj)
	if err != nil {
		printErr("updateComicsPage", err)
		return err
	}

	var (
		dbComic comic
		isNew bool
		)

	for _, c := range jsonObj.Data.Comics.Docs {
		isNew = false

		err := db.QueryRow("SELECT PAGES,IS_FINISHED FROM COMICS WHERE ID = ?",c.Id).Scan(&dbComic.PagesCount,&dbComic.Finished)
		if err != nil {
			isNew = true
		}else{
			if c.PagesCount != dbComic.PagesCount || c.Finished != dbComic.Finished {
				isNew = true
			}
		}

		if isNew{
			_, err := db.Exec("REPLACE INTO COMICS(ID,TITLE,PAGES,IS_FINISHED,IS_NEW,IS_DOWN,LABELS) VALUES(?,?,?,?,?,?,?)",c.Id,reg.ReplaceAllString(c.Title,"-"),c.PagesCount,c.Finished,1,0,strings.Join(c.Categories," "))
			if err != nil {
				printErr("updateComicsPage", err)
				return err
			}
		}
	}

	if jsonObj.Data.Comics.Pages != page{
		updateComicsPage(search, page+1,db)
	}
	return nil
}

//最新未下载已完结本子
func newFinishedComics(db *sql.DB, isDown bool, keyWord string)error{
	var (
		err error
		comics = make(map[int]string)
		)
	if db == nil{
		db,err = sql.Open("sqlite3", dbPath)
		defer db.Close()
		if err != nil {
			printErr("newFinishedComics", err)
			return err
		}
	}

	sqlEx := ""
	if keyWord != ""{
		sqlEx = " AND LABELS LIKE '%"+keyWord+"%'"
	}
	rows,err := db.Query("SELECT ID,TITLE,PAGES,LABELS FROM COMICS WHERE IS_DOWN = 0 AND IS_FINISHED = 1"+sqlEx)
	if err != nil {
		printErr("newFinishedComics", err)
		return err
	}

	var (
		title string
		id string
		index = 0
		comicPics int
		labels string
		)
	for rows.Next(){
		_ = rows.Scan(&id, &title, &comicPics, &labels)
		if !isDown{
			println(id + " " + title + " " + strconv.Itoa(comicPics) + " [" + labels+"]")
		}else{
			comics[index] = id
			index++
		}
	}

	for _,c := range comics{
		downComic(c,db)
	}

	return nil
}

//下载本子
func downComic(comicId string, db *sql.DB)error{
	var err error
	if db == nil{
		db,err = sql.Open("sqlite3", dbPath)
		defer db.Close()
		if err != nil {
			printErr("newFinishedComics", err)
			return err
		}
	}

	updateComicPics(comicId,db)

	_,err=db.Exec("UPDATE COMICS SET IS_DOWN = 1 WHERE ID = ?",comicId)
	if err != nil {
		printErr("newFinishedComics", err)
		return err
	}

	println("完成: "+comicId)
	println("-------------------------------")
	return nil
}

//下载失败的本子
func downFailComics(comicIds string)(map[string]string, error){
	db,err := sql.Open("sqlite3", dbPath)
	defer db.Close()
	if err != nil {
		printErr("downFailComics", err)
		return nil, err
	}

	var sql = "SELECT DISTINCT COMIC_ID FROM PICS WHERE IS_DOWN = 0"
	if comicIds != ""{
		sql = "SELECT DISTINCT COMIC_ID FROM PICS WHERE IS_DOWN = 0 AND COMIC_ID IN ('"+comicIds+"')"
	}
	rows,err := db.Query(sql)
	if err != nil {
		printErr("downFailComics", err)
		return nil, err
	}

	var id string
	var list = make(map[string]string)
	for rows.Next(){
		rows.Scan(&id)
		list[id]=""
	}
	return list, nil
}

//更新本子图片信息
func updateComicPics(comicId string,db *sql.DB)error{
	var err error
	picCount := 0
	if db == nil{
		db,err = sql.Open("sqlite3", dbPath)
		defer db.Close()
		if err != nil {
			printErr("newFinishedComics", err)
			return err
		}
	}

	tx,err :=db.Begin()
	if err != nil {
		printErr("newFinishedComics", err)
		return err
	}

	stmt,err := tx.Prepare("REPLACE INTO PICS(ID,PATH,COMIC_ID,IS_DOWN) VALUES(?,?,?,?)")
	if err != nil {
		printErr("newFinishedComics", err)
		return err
	}
	defer stmt.Close()

	//println("正在更新本子图片信息:0")

	page,pages := 1,1

	for page<=pages{
		response,err := picaRequest("comics/"+comicId+"/eps?page="+strconv.Itoa(page), "GET")
		if err != nil {
			printErr("updateComicPics", err)
			return err
		}

		var epObj comicPage
		err = json.Unmarshal([]byte(response), &epObj)
		if err != nil {
			printErr("updateComicPics", err)
			return err
		}

		for _, ep := range epObj.Data.Eps.Docs {
			picPage,picPages := 1,1
			for picPage<=picPages{
				picResponse,err := picaRequest("eps/"+ep.Id+"/pages?page="+strconv.Itoa(picPage), "GET")
				var picObj epDown
				err = json.Unmarshal([]byte(picResponse), &picObj)
				if err != nil {
					printErr("updateComicPics", err)
					return err
				}

				for _, pic := range picObj.Data.Pages.Docs {
					_,err = stmt.Exec(pic.Id,pic.Media.Path,comicId,0)
					if err != nil {
						printErr("updateComicPics", err)
						return err
					}
					picCount++
					fmt.Fprintf(os.Stdout,"正在更新本子图片信息:%d\r", picCount)
				}

				picPage++
				picPages = picObj.Data.Pages.Pages
			}
		}

		pages=epObj.Data.Eps.Pages
		page++
	}

	tx.Commit()
	fmt.Println("")

	downComicPics(comicId,db,picCount)
	fmt.Println("")
	return nil
}

//下载本子图片
func downComicPics(comicId string,db *sql.DB,picCount int)error{
	errCnt := 0

	//println("正在下载本子图片: 0/%d",picCount)
	var err error
	if db == nil{
		db,err = sql.Open("sqlite3", dbPath)
		defer db.Close()
		if err != nil {
			printErr("downComicPics", err)
			return err
		}
	}

	rows,err := db.Query("SELECT ID, PATH FROM PICS WHERE COMIC_ID = ? ORDER BY ID",comicId)
	if err != nil {
		printErr("downComicPics", err)
		return err
	}
	defer rows.Close()

	var title string
	err = db.QueryRow("SELECT TITLE FROM COMICS WHERE ID = ?",comicId).Scan(&title)
	if err != nil {
		printErr("downComicPics", err)
		return err
	}

	var (
		index = 1
		picPath string
		savePath = path+title
		picId string
		successPic = make(map[int]string)
		)

	if !fileExists(savePath) {
		os.MkdirAll(savePath, os.ModePerm)
	}
	for rows.Next(){
		err = rows.Scan(&picId, &picPath)
		if err != nil {
			printErr("downComicPics", err)
			return err
		}

		err = downPic(&picPath, fmt.Sprintf("%04d.jpg", index), &savePath, &picId)
		if err != nil {
			err = downPic(&picPath, fmt.Sprintf("%04d.jpg", index), &savePath, &picId)
			if err == nil {
				successPic[index]=picId
			}else{
				//fmt.Printf("下载失败: %04d.jpg %s\r\n", index, imgServer+picPath)
				errCnt++
			}
		}else{
			successPic[index]=picId
		}
		fmt.Fprintf(os.Stdout,"正在下载本子图片: %d/%d\r",index, picCount)
		index++
	}

	rows.Close()

	tx,err:=db.Begin()
	if err != nil {
		printErr("downComicPics", err)
		return err
	}
	stmt,err:=tx.Prepare("UPDATE PICS SET IS_DOWN = 1 WHERE ID = ?")
	defer stmt.Close()
	if err != nil {
		printErr("downComicPics", err)
		return err
	}

	for _,pic := range successPic {
		_,err:=stmt.Exec(pic)
		if err != nil {
			printErr("downComicPics", err)
			return err
		}
	}
	tx.Commit()

	if errCnt > 0{
		fmt.Println(" error:" + strconv.Itoa(errCnt))
	}

	return nil
}

//下载图片
func downPic(url *string, name string, savePath *string, picId *string)error{
	var picSavePath = *savePath + "\\" + name
	if !fileExists(picSavePath){
		resp, err := http.DefaultClient.Get(imgServer + *url)
		if err != nil {
			//printErr("downPic", err)
			//println("---get: "+picSavePath)
			return err
		}
		defer resp.Body.Close()

		file, err := os.Create(picSavePath)
		defer file.Close()
		if err != nil {
			//printErr("downPic", err)
			//println("---os.Create: "+picSavePath)
			file.Close()
			err2:=os.Remove(picSavePath)
			if err2 != nil{
				//printErr("downPic", err)
			}
			return err
		}

		_, err = io.Copy(file, resp.Body)
		if err != nil {
			//printErr("downPic", err)
			//println("---io.Copy: "+picSavePath)
			file.Close()
			err2:=os.Remove(picSavePath)
			if err2 != nil{
				//printErr("downPic", err)
			}
			return err
		}
	}
	return nil
}

//遍历db
func iteratorDb() error{
	db,err := sql.Open("sqlite3", dbPath)
	defer db.Close()
	if err != nil {
		printErr("iteratorDb", err)
		return err
	}else{
		rows,err := db.Query("SELECT TITLE FROM COMICS")
		if err != nil {
			printErr("iteratorDb", err)
			return err
		}

		var title string
		for rows.Next(){
			_ = rows.Scan(&title)
			println(title)
		}
	}

	return nil
}

func t(){
	response,err := picaRequest("comics?page=1&c=WEBTOON", "GET")
	if err != nil{
		printErr("updateComicsPage", err)
	}

	println(response)
}


func picaRequest(url string, method string) (responseData string, err error) {
	//request
	req, err := http.NewRequest(method, urlHost+url, nil)
	if err != nil {
		printErr("picaRequest", err)
		return "", err
	}
	//base header
	setRequestBaseHeader(req)

	//sign header
	t, s := sign(url, method)
	setRequestSignatureHeader(req, t, nonce, s)

	//response
	resp, err := http.DefaultClient.Do(req)
	defer resp.Body.Close()

	//response body
	var reader io.ReadCloser
	if resp.Header.Get("Content-Encoding") == "gzip" {
		reader, err = gzip.NewReader(resp.Body)
		if err != nil {
			printErr("picaRequest", err)
			return "", err
		}
	} else {
		reader = resp.Body
	}
	body, err := ioutil.ReadAll(reader)
	if err != nil {
		printErr("picaRequest", err)
		return "", err
	}

	responseData = string(body)
	//println(responseData)

	var jsonObj response
	err = json.Unmarshal([]byte(responseData), &jsonObj)
	if err != nil {
		printErr("picaRequest", err)
		return "", err
	}
	if jsonObj.Code == 401 {
		err = picaLogin()
		if err != nil{
			printErr("picaRequest", err)
			return "", err
		}
		responseData,_ = picaRequest(url, method)
	}

	//println(responseData)
	return responseData, nil
}

func picaLogin()(error) {
	//request
	req, err := http.NewRequest("POST", urlHost+"auth/sign-in", strings.NewReader(`{"email":"kekeslife","password":"52711269"}`))
	if err != nil {
		printErr("picaLogin", err)
		return err
	}
	//base header
	setRequestBaseHeader(req)
	req.Header.Set("Content-Type", "application/json; charset=UTF-8")

	//sign header
	t, s := sign("auth/sign-in", "POST")
	setRequestSignatureHeader(req, t, nonce, s)

	//response
	resp, err := http.DefaultClient.Do(req)
	defer resp.Body.Close()

	//response body
	var reader io.ReadCloser
	if resp.Header.Get("Content-Encoding") == "gzip" {
		reader, err = gzip.NewReader(resp.Body)
		if err != nil {
			printErr("picaLogin", err)
			return err
		}
	} else {
		reader = resp.Body
	}
	body, err := ioutil.ReadAll(reader)
	if err != nil {
		printErr("picaLogin", err)
		return err
	}

	responseData := string(body)

	var jsonObj login
	err = json.Unmarshal([]byte(responseData), &jsonObj)
	if err != nil {
		printErr("picaLogin", err)
		return err
	}
	if jsonObj.Code == 200 {
		authorization = jsonObj.Data.Token
	} else {
		println(responseData)
		fmt.Println("登录错误")
	}

	return nil
}

func sign(url string, method string) (timestamp string, sign string) {
	//key := "~n}$S9$lGts=U)8zfL/R.PM9;4[3|@/CEsl~Kk!7?BYZ:BAa5zkkRBL7r|1/*Cr"
	key := "~d}$Q7$eIni=V)9\\RK/P.RM4;9[7|@/CA}b~OW!3?EV`:<>M7pddUBL5n|0/*Cn"
	timestamp = strconv.FormatInt(time.Now().Unix(), 10)
	pars := strings.ToLower(url + timestamp + nonce + method + appKey)
	h := hmac.New(sha256.New, []byte(key))
	h.Write([]byte(pars))
	sign = hex.EncodeToString(h.Sum(nil))
	//println(pars)
	//println(sign)
	return
}

func setRequestBaseHeader(req *http.Request) {
	req.Header.Set("authorization", authorization)
	req.Header.Set("api-key", "C69BAF41DA5ABD1FFEDC6D2FEA56B")
	req.Header.Set("accept", "application/vnd.picacomic.com.v1+json")
	req.Header.Set("app-channel", "1")
	req.Header.Set("app-version", "2.2.1.2.3.3")
	req.Header.Set("app-uuid", "d7028740-bb9b-3a91-84cd-7e6bae5a8e8e")
	req.Header.Set("app-platform", "android")
	req.Header.Set("app-build-version", "44")
	req.Header.Set("User-Agent", "okhttp/3.8.1")
	req.Header.Set("Connection", "Keep-Alive")
	req.Header.Set("Accept-Encoding", "gzip")
}

func setRequestSignatureHeader(req *http.Request, time string, nonce string, signature string) {
	req.Header.Set("signature", signature)
	req.Header.Set("nonce", nonce)
	req.Header.Set("time", time)
}

func fileExists(path string) bool {
	if _, err := os.Stat(path); err != nil {
		if os.IsNotExist(err) {
			return false
		}
	}
	return true
}

type comic struct {
	Id         string `json:"_id"`
	Title      string
	Desc       string
	Categories []string
	EpsCount   int //集数
	PagesCount int //图片数
	Finished   bool
	CreatedAt  time.Time
	UpdateAt   time.Time
	Thumb      struct {
		Path string
	}
	IsNew bool
}

type response struct {
	Code    int
	Message string
}

type login struct {
	response
	Data struct {
		Token string
	}
}

type hot24Response struct {
	response
	Data struct {
		Comics []comic
	}
}

type searchResponse struct {
	response
	Data struct {
		Comics struct {
			Docs []comic
			Pages int
			Page  int
			Total int
		}
	}
}

type comicPage struct {
	response
	Data struct {
		Eps struct {
			Docs  []ep
			Total int
			Page  int
			Pages int
		}
	}
}

type ep struct {
	Id    string `json:"_id"`
	Title string
	Order int
}

type epDown struct {
	response
	Data struct {
		Pages struct {
			Docs []struct {
				Id    string `json:"_id"`
				Media struct {
					OriginalName string
					Path         string
				}
			}
			Pages int
		}
		ep ep
	}
}
